# PyTribe: classes to communicate with EyeTribe eye trackers
#
# author: Edwin Dalmaijer
# email: edwin.dalmaijer@psy.ox.ac.uk
#
# version 4 (21-Jun-2016)

import os
import copy
import json
import time
import socket
import codecs
from threading import Lock, Thread
from multiprocessing import Event, Process, Queue
from pygaze.py3compat import *


# # # # #
# EYETRIBE CLASS

# The original EyeTribe class from earlier versions of PyTribe.
class EyeTribe:

	"""class for eye tracking and data collection using an EyeTribe tracker
	"""

	def __init__(self, logfilename='default', host='localhost', port=6555):

		"""Initializes an EyeTribe instance

		keyword arguments

		logfilename	--	string indicating the log file name, including
						a full path to it's location and an extension
						(default = 'default.txt')
		"""
		
		# initialize data collectors
		self._logfile = codecs.open('%s.tsv' % (logfilename), 'w', u'utf-8')
		self._separator = u'\t'
		self._log_header()
		self._queue = Queue()

		# initialize connection
		self._connection = connection(host=host, port=port)
		self._tracker = tracker(self._connection)
#		self._tracker.set_push(push=False)
		self._heartbeat = heartbeat(self._connection)

		# create a new Lock
		self._lock = Lock()

		# initialize heartbeat thread
		self._beating = True
		self._heartbeatinterval = self._tracker.get_heartbeatinterval() / 1000.0
		self._hbthread = Thread(target=self._heartbeater, args=[self._heartbeatinterval])
		self._hbthread.daemon = True
		self._hbthread.name = 'heartbeater'

		# initialize sample streamer
		self._streaming = True
		self._samplefreq = self._tracker.get_framerate()
		self._intsampletime = 1.0 / self._samplefreq
		self._clockdiff = None
		self._newestframe = self._tracker.get_frame()
		self._ssthread = Thread(target=self._stream_samples, args=[self._queue])
		self._ssthread.daemon = True
		self._ssthread.name = 'samplestreamer'

		# initialize data processer
		self._processing = True
		self._processing_paused = False
		self._logdata = False
		self._currentsample = copy.deepcopy(self._newestframe)
		self._dpthread = Thread(target=self._process_samples, args=[self._queue])
		self._dpthread.daemon = True
		self._dpthread.name = 'dataprocessor'

		# start all threads
		self._hbthread.start()
		self._ssthread.start()
		self._dpthread.start()

		# initialize calibration
		self.calibration = calibration(self._connection)

	def start_recording(self):

		"""Starts data recording
		"""

		# set self._logdata to True, so the data processing thread starts
		# writing samples to the log file
		if not self._logdata:
			self._logdata = True
			self.log_message("start_recording")

	def stop_recording(self):

		"""Stops data recording
		"""

		# consolidate the data file on the hard drive
		# internal buffer to RAM
		self._logfile.flush()
		# RAM file cache to disk
		os.fsync(self._logfile.fileno())

		# set self._logdata to False, so the data processing thread does not
		# write samples to the log file
		if self._logdata:
			self.log_message("stop_recording")
			self._logdata = False

	def log_message(self, message):

		"""Logs a message to the logfile, time locked to the most recent
		sample
		"""
		
		# Get the current time.
		t = time.time()
		# Make a string in the specific format that the EyeTribe uses:
		# yyyy-mm-dd HH:MM:SS.000
		ts = '%s.%d' % (time.strftime('%Y-%m-%d %H:%M:%S'), round(t % 1, 3)*1000)
		
		# Correct the time to EyeTribe time
		if self._clockdiff != None:
			t = int(t*1000 + self._clockdiff)
		else:
			t = ''
		# assemble line
		line = self._separator.join(map(str,[u'MSG', ts, t, safe_decode(message)]))
		# write message
		self._logfile.write(line + u'\n') # to internal buffer

	def sample(self):

		"""Returns the most recent point of regard (=gaze location on screen)
		coordinates (smoothed signal)

		arguments

		None

		returns

		gaze		--	a (x,y) tuple indicating the point of regard
		"""

		if self._newestframe == None:
			return None, None
		else:
			return (self._newestframe['avgx'],self._newestframe['avgy'])

	def pupil_size(self):

		"""Returns the most recent pupil size sample (an average of the size
		of both pupils)

		arguments

		None

		returns

		pupsize	--	a float indicating the pupil size (in arbitrary units)
		"""

		if self._currentsample == None:
			return None
		else:
			return self._newestframe['psize']

	def close(self):

		"""Stops all data streaming, and closes both the connection to the
		tracker and the logfile
		"""

		# if we are currently recording, stop doing so
		if self._logdata:
			self.stop_recording()

		# signal all threads to halt
		self._beating = False
		self._streaming = False
		self._processing = False

		# close the log file
		self._logfile.close()

		# close the connection
		self._connection.close()

	def _pause_sample_processing(self):

		"""Halts the processing of samples. Make sure to call this when
		you calibrate the tracker.
		"""

		self._processing_paused = True

	def _unpause_sample_processing(self):

		"""Continues the processing of samples. Make sure to call this when
		you are done calibrating the tracker.
		"""

		self._processing_paused = False

	def _heartbeater(self, heartbeatinterval):

		"""Continuously sends heartbeats to the tracker, to let it know the
		connection is still alive (it seems to think we could die any
		moment now, and is very keen on reassurance of our good health;
		almost like my grandparents...)

		arguments

		heartbeatinterval	--	float indicating the heartbeatinterval in
							seconds; note that this is different from
							the value that the EyeTribe tracker reports:
							that value is in milliseconds and should be
							recalculated to seconds here!
		"""

		# keep beating until it is signalled that we should stop
		while self._beating:
			# do not bother the tracker when it is calibrating
			#self._wait_while_calibrating()
			# wait for the Threading Lock to be released, then lock it
			self._lock.acquire(True)
			# send heartbeat
			self._heartbeat.beat()
			# release the Threading Lock
			self._lock.release()
			# wait for a bit
			time.sleep(heartbeatinterval)

	def _stream_samples(self, queue):

		"""Continuously polls the device, and puts all new samples in a
		Queue instance

		arguments

		queue		--	a multithreading.Queue instance, to put samples
						into
		"""

		# keep streaming until it is signalled that we should stop
		while self._streaming:
			# Only get a sample if the processing is not paused
			if not self._processing_paused:
				# wait for the Threading Lock to be released, then lock it
				self._lock.acquire(True)
				# get a new sample
				sample = self._tracker.get_frame()
				t1 = time.time() * 1000
				# put the sample in the Queue
				queue.put(sample)
				# release the Threading Lock
				self._lock.release()
				# Update the newest frame
				self._newestframe = copy.deepcopy(sample)
				# Calculate the clock difference
				self._clockdiff = sample['time'] - t1
			# pause for half the intersample time, to avoid an overflow
			# (but to make sure to not miss any samples)
			time.sleep(self._intsampletime/2)

	def _process_samples(self, queue):

		"""Continuously processes samples, updating the most recent sample
		and writing data to a the log file when self._logdata is set to True

		arguments

		queue		--	a multithreading.Queue instance, to read samples
						from
		"""

		# keep processing until it is signalled that we should stop
		while self._processing:
			# wait for the Threading Lock to be released, then lock it
			self._lock.acquire(True)
			# read new item from the queue
			if not queue.empty():
				sample = queue.get()
			else:
				sample = None
			# release the Threading Lock
			self._lock.release()
			# update newest sample
			if sample != None:
				# check if the new sample is the same as the current sample
				if not self._currentsample['timestamp'] == sample['timestamp']:
					# update current sample
					self._currentsample = copy.deepcopy(sample)
					# write to file if data logging is on
					if self._logdata:
						self._log_sample(sample)

	def _log_sample(self, sample):

		"""Writes a sample to the log file

		arguments

		sample		--	a sample dict, as is returned by
						tracker.get_frame
		"""

		# assemble new line
		line = self._separator.join(map(str,[	sample['timestamp'],
										sample['time'],
										sample['fix'],
										sample['state'],
										sample['rawx'],
										sample['rawy'],
										sample['avgx'],
										sample['avgy'],
										sample['psize'],
										sample['Lrawx'],
										sample['Lrawy'],
										sample['Lavgx'],
										sample['Lavgy'],
										sample['Lpsize'],
										sample['Lpupilx'],
										sample['Lpupily'],
										sample['Rrawx'],
										sample['Rrawy'],
										sample['Ravgx'],
										sample['Ravgy'],
										sample['Rpsize'],
										sample['Rpupilx'],
										sample['Rpupily']
								]))
		# write line to log file
		self._logfile.write(line + '\n') # to internal buffer

	def _log_header(self):

		"""Logs a header to the data file
		"""

		# write a header to the data file
		header = self._separator.join(['timestamp','time','fix','state',
								'rawx','rawy','avgx','avgy','psize',
								'Lrawx','Lrawy','Lavgx','Lavgy','Lpsize','Lpupilx','Lpupily',
								'Rrawx','Rrawy','Ravgx','Ravgy','Rpsize','Rpupilx','Rpupily'
								])
		self._logfile.write(header + '\n') # to internal buffer
		self._logfile.flush() # internal buffer to RAM
		os.fsync(self._logfile.fileno()) # RAM file cache to disk
		self._firstlog = False



# # # # # #
# PARALLEL ClASS


# Ugly, but sod it: A global variable for the most recent sample.
global _current_sample


# Class to communicate with an EyeTribe tracker. The actual communications
# and logging actually run in a separate Process. This class just sends
# commands to that Process.
class ParallelEyeTribe:
	
	def __init__(self, logfilename='default'):
		
		# Set some standard stuff (hard coded now, but can potentially
		# be passed to the __init__ method in the future.)
		host = 'localhost'
		port = 6555
		
		# We need an Event that signals whether the connection to the
		# EyeTribe is supposed to be open.
		self._connection_alive = Event()
		self._connection_alive.set()
		
		# We also need a Queue to send commands through.
		self._command_queue = Queue()
		# And we need a Queue to receive commands through.
		self._to_main_queue = Queue()
		
		# Start a parallel process that will take care of all EyeTribe
		# things. It will provide regular heartbeats to keep the connection
		# alive, it will record gaze data to a file, and it will keep the
		# most recent sample updated. This is all done in a separate
		# Process (rather than in Threads) to so that it can be offloaded
		# to a different CPU core. This prevents the ongoing experiment
		# (or whatever you're doing in the main Thread) from interfering
		# with the processing (and recording) of gaze data.
		self.eyetribe_process = Process(target=_run_eyetribe_process, \
			args=[logfilename, host, port, self._connection_alive, \
			self._command_queue])
		self.eyetribe_process.name = u'pygaze_eyetribe'
		self.eyetribe_process.daemon = True
		self.eyetribe_process.start()

	def start_recording(self):

		"""Starts data recording
		"""
		
		# Send a command to the EyeTribe Process
		self._command_queue.put(('start_recording', ()))

	def stop_recording(self):

		"""Stops data recording
		"""

		# Send a command to the EyeTribe Process
		self._command_queue.put(('stop_recording', ()))

	def log_message(self, message):

		"""Logs a message to the logfile, time locked to the most recent
		sample
		"""

		# Send a command to the EyeTribe Process
		self._command_queue.put(('log_message', (message)))

	def sample(self):

		"""Returns the most recent point of regard (=gaze location on screen)
		coordinates (smoothed signal)

		arguments

		None

		returns

		gaze		--	a (x,y) tuple indicating the point of regard
		"""
		
		global _current_sample
		
		if _current_sample == None:
			return None, None
		else:
			return (_current_sample['avgx'], _current_sample['avgy'])

	def pupil_size(self):

		"""Returns the most recent pupil size sample (an average of the size
		of both pupils)

		arguments

		None

		returns

		pupsize	--	a float indicating the pupil size (in arbitrary units)
		"""

		global _current_sample

		if _current_sample == None:
			return None
		else:
			return _current_sample['psize']

	def close(self):

		"""Stops all data streaming, and closes both the connection to the
		tracker and the logfile
		"""

		# Send a command to the EyeTribe Process
		self._command_queue.put(('close', ()))


# Function that can run in a parallel process, to keep a connection with the
# EyeTribe open, and to log data when appropriate.
def _run_eyetribe_process(logfilename, host, port, connection_alive, command_queue):
		
	"""FOR INTERNAL USE ONLY
	"""
	
	# Ugly, but sod it: A global variable for the most recent sample.
	global _current_sample
	
	# Initialise a new _EyeTribe instance to open the connection to the
	# EyeTribe.
	tracker = EyeTribe(logfilename=logfilename, host=host, port=port)
	
	# Run until the connection is closed.
	while connection_alive.is_set():
		
		# Check the incoming Queue.
		if not command_queue.empty():
			# Get the oldest command in the Queue. This is a tuple that
			# contains a string (the command), and a tuple of values
			# (what they are depends on the specific command).
			cmd, value = command_queue.get()
			
			# Start recording.
			if cmd == 'start_recording':
				tracker.start_recording()

			# Stop recording.
			elif cmd == 'stop_recording':
				tracker.stop_recording()

			# Log a message.
			elif cmd == 'log_message':
				tracker.log_message(value[0])

			# Close the connection to the EyeTribe.
			elif cmd == 'close':
				tracker.close()
				# Unset the Event that signals that the connection is
				# alive.
				connection_alive.clear()
		
		# Update the current sample.
		_current_sample = copy.deepcopy(tracker._currentsample)


# # # # #
# SUPPORTING CLASSES

class connection:

	"""class for connections with the EyeTribe tracker"""

	def __init__(self, host='localhost', port=6555):

		"""Initializes the connection with the EyeTribe tracker

		keyword arguments

		host		--	a string indicating the host IP, NOTE: currently only
					'localhost' is supported (default = 'localhost')
		port		--	an integer indicating the port number, NOTE: currently
					only 6555 is supported (default = 6555)
		"""

		# Define properties for this connection.
		self.host = host
		self.port = port
		self.DEBUG = False

		# Dict for the most recent responses. This dict is updated whenever
		# a new response comes in from the tracker. It has top-level keys
		# for each category ('tracker', 'calibration', and 'heartbeat'),
		# second-level keys for each potential request (e.g. 'set' and
		# 'get' for the tracker category), and third-level keys for each
		# subtype of request (e.g. 'frame' for the 'get' request in the
		# 'tracker' category).
		self._responses = { \
			'tracker':		{ \
				'get':	{ \
					'push':			None,
					'heartbeatinterval':	None,
					'version':			None,
					'trackerstate':		None,
					'framerate':		None,
					'iscalibrated':		None,
					'iscalibrating':		None,
					'calibresult':		None,
					'frame':			None,
					'screenindex':		None,
					'screenresw':		None,
					'screenresh':		None,
					'screenpsyw':		None,
					'screenpsyh':		None
					}, \
				'set':	{ \
					'push':			None,
					'heartbeatinterval':	None,
					'version':			None,
					'trackerstate':		None,
					'framerate':		None,
					'iscalibrated':		None,
					'iscalibrating':		None,
					'calibresult':		None,
					'frame':			None,
					'screenindex':		None,
					'screenresw':		None,
					'screenresh':		None,
					'screenpsyw':		None,
					'screenpsyh':		None
					} \
				}, \
			'calibration':	{ \
				'start':				None, \
				'pointstart':			None, \
				'pointend':				None, \
				'abort':				None, \
				'clear':				None \
				}, \
			'heartbeat':				None \
			}

		# The reponse timeout (in seconds) determines how long we should
		# wait for the relevant _responses dict field to be populated,
		# after sending a request over the connection.		
		self.response_timeout = 1.0

		# Initialize a connection with the EyeTribe server.
		self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self.sock.connect((self.host,self.port))

		# Create a request Lock to prevent simultaneous access to the
		# connection.
		self._request_lock = Lock()
		
		# Open a DEBUG text file in DEBUG mode. Also create a Lock, to
		# prevent separate Threads simultaneously writing to the file.
		if self.DEBUG:
			self._debugfile = open('DEBUG_output.txt', 'w')
			t = time.time()
			self._debugfile.write("PYTRIBE DEBUG OUTPUT %s (%d)" % \
				(time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(t)), t*1000))
			self._debuglock = Lock()
		
		# Launch a Thread that monitors the connection, and processes every
		# response from the EyeTribe that comes in over the connection.
		self._listening = True
		self._listenthread = Thread(target=self._get_responses, args=[])
		self._listenthread.daemon = True
		self._listenthread.name = 'listener'
		self._listenthread.start()

	def request(self, category, request, values):

		"""Send a message over the connection

		arguments

		category	--	string indicating the query category
		request	--	string indicating the actual request of the message
		values	--	dict or list containing parameters of the request
		"""
		
		# Create a JSON-formatted string for the current request.
		msg = self.create_json(category, request, values)
		
		# Acquire the lock, to prevent simultaneous access.
		self._request_lock.acquire()
		# Clear the response that's currently in memory.
		if category == 'heartbeat':
			self._responses[category] = None
		elif category == 'calibration':
			self._responses[category][request] = None
		elif category == 'tracker':
			self._responses[category][request][values[0]] = None
		# Release the lock, to allow other Threads to access the
		# _responses dict again.
		self._request_lock.release()

		# Send the JSON-formatted message over the connection.
		self._request_lock.acquire()
		self.sock.send(msg)
		self._request_lock.release()

		# Store request in DEBUG mode.
		if self.DEBUG:
			self._debuglock.acquire()
			self._debugfile.write("\nREQUEST (%d): '%s'" % (time.time()*1000, msg))
			self._debuglock.release()
		
		# Wait until a response is available.
		r = None
		t0 = time.time()
		success = False
		while time.time() - t0 <= self.response_timeout:
			# Acquire the lock, to prevent simultaneous access.
			self._request_lock.acquire()
			# Check if a response is available.
			if category == 'heartbeat':
				r = self._responses[category]
			elif category == 'calibration':
				r = self._responses[category][request]
			elif category == 'tracker':
				r = self._responses[category][request][values[0]]
			# Release the lock, to allow other Threads to access the
			# _responses dict again.
			self._request_lock.release()
			# Wait for a bit if no response is available.
			if r == None:
				time.sleep(0.005)
			else:
				success = True
				break

		# If we couldn't find the response that matches our request, return
		# a 404 status message.
		if not success:
			r = self.parse_json('{"statuscode":404,"values":{"statusmessage":"could not find response to this request"}}')

		# Parse and return the response.
		return r

	def create_json(self, category, request, values):

		"""Creates a new json message, in the format that is required by the
		EyeTribe tracker; these messages consist of a categort, a request and
		a (list of) value(s), which can be thought of as class.method.value
		(for more info, see: http://dev.theeyetribe.com/api/)

		arguments

		category	--	query category (string), e.g. 'tracker',
					'calibration', or 'heartbeat'
		request	--	the request message (string), e.g. 'get' for the
					'tracker' category
		values	--	a dict of parameters and their values, e.g.
					{"push":True, "version":1}
					OR:
					a list of parameters, e.g. ['push','iscalibrated']
					OR:
					None to pass no values at all

		keyword arguments

		None

		returns

		jsonmsg	--	a string in json format, that can be directly sent to
					the EyeTribe tracker
		"""

		# error if the values are anything other than a dict, tuple or list
		if values is not None and type(values) not in [dict, list, tuple]:
			raise Exception("values should be dict, tuple or list, not '%s' (values = %s)" % (type(values),values))

		# create the json message
		if request == None:
			jsondict = {"category":category}
		elif values == None:
			jsondict = {"category":category, "request":request}
		else:
			jsondict = {"category":category, "request":request, "values":values}

		return json.dumps(jsondict)

	def parse_json(self, jsonmsg):

		"""Parses a json message as those that are usually returned by the
		EyeTribe tracker
		(for more info, see: http://dev.theeyetribe.com/api/)

		arguments

		jsonmsg	--	a string in json format

		keyword arguments

		None

		returns

		msg		--	a dict containing the information in the json message;
					this dict has the following content:
						{	"category":	"tracker",
							"request":	"get",
							"statuscode":	200,
							"values":	{	"push":True,
										"iscalibrated":True
										}
							}
		"""

		# parse json message
		parsed = json.loads(jsonmsg)

		return parsed

	def revive(self):

		"""Re-establishes a connection
		"""
		
		# Acquire the request Lock, to prevent simultaneous access to the
		# connection while we are reviving it.
		self._request_lock.acquire()

		# Close old connection.
		self.close()

		# Initialize a new connection.
		self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		self.sock.connect((self.host,self.port))

		# Release the lock, to allow other Threads to access the new
		# connection.
		self._request_lock.release()

	def close(self):

		"""Closes the connection to the EyeTribe tracker
		"""
		
		# Stop the sub-Thread that listens to the EyeTribe and processes
		# its responses.
		self._listening = False

		# Close the socket connection.
		self.sock.close()


	def _get_responses(self):

		"""INTERNAL USE ONLY. This method is run in a sub-Thread, to listen
		to the EyeTribe, and to process all its responses. Responses are
		stored in a 
		"""
		
		# Create a variable to store any unfinished responses.
		unfinished = ''
		
		# Run until self._listening turns False.
		while self._listening:
			
			# Try to get a response from the connection.
			try:
				responses = self.sock.recv(32768)
			# If it fails, revive the connection and register a
			# connection error.
			except socket.error:
				print("reviving connection")
				self.revive()
				responses = '{"statuscode":901,"values":{"statusmessage":"connection error"}}'
			
			# Store the raw responses in DEBUG mode.
			if self.DEBUG:
				self._debuglock.acquire()
				self._debugfile.write("\nRAWRESPONSES (%d): '%s'" % (time.time()*1000, responses))
				self._debuglock.release()

			# Split the responses (usually, multiple come in at once).
			responses = responses.split('\n')

			# Parse the responses.
			for i, r in enumerate(responses):

				# Skip the response if it's empty.
				if r == '':
					continue

				# Reset the resp variable.
				resp = None

				# Try to parse the response.
				try:
					resp = self.parse_json(r)
				# If parsing the response fails, we probably received
				# a half-formed response.
				except:
					# If we currently don't have half a respons
					# stored, then the current response might well be
					# the first half of a half-formed response.
					if unfinished == '':
						# Store the unfinished response.
						unfinished = copy.deepcopy(r)
					# If we do have an unfinished response stored, try
					# to match it up with the current half-formed
					# response.
					else:
						# Combine + parse the stored unfinished
						# response, and the incoming half-formed
						# response.
						try:
							resp = self.parse_json(unfinished + r)
						# If we can't use the stored unfinished
						# response and the incoming half-formed one,
						# just do nothing.
						except:
							pass
						# Reset the unfinished response.
						unfinished = ''
				
				# In DEBUG mode, store the parsed response.
				if self.DEBUG:
					self._debuglock.acquire()
					self._debugfile.write("\nPARSEDRESPONSE (%d): '%s'" % (time.time()*1000, resp))
					self._debuglock.release()

				# Store the parsed response in the _responses dict.
				if resp != None:
					if 'category' in resp.keys():
						self._request_lock.acquire()
						if resp['category'] == 'heartbeat':
							self._responses[resp['category']] = \
								copy.deepcopy(resp)
						elif resp['category'] == 'calibration':
							if 'request' in resp.keys():
								# Special category: if the
								# request was 'pointend', the
								# 'calibresult' can be returned.
								# We want to store this in the
								# tracker.get.calibresult entry
								# too.
								self._responses[resp['category']][resp['request']] = \
									copy.deepcopy(resp)
								if resp['request'] == 'pointend' and \
									'values' in resp.keys() and \
									'calibresult' in resp['values'].keys():
									self._responses['tracker']['get']['calibresult'] = \
										copy.deepcopy(resp)
							else:
								print("Could not store response: '%s'" % (resp))
						elif resp['category'] == 'tracker':
							if ('request' in resp.keys()) and ('values' in resp.keys()):
								for k in resp['values'].keys():
									self._responses[resp['category']][resp['request']][k] = \
										copy.deepcopy(resp)
							else:
								print("Could not store response: '%s'" % (resp))
						self._request_lock.release()
					else:
						print("Could not store response: '%s'" % (resp))


class tracker:

	"""class for SDK Tracker state and information related requests"""

	def __init__(self, connection):

		"""Initializes a tracker instance

		arguments

		connection	--	a pytribe.connection instance for the currently
						attached EyeTribe tracker
		"""

		self.connection = connection
		self.push = True

	def set_connection(self, connection):

		"""Set a new connection

		arguments

		connection	--	a pytribe.connection instance for the currently
						attached EyeTribe tracker
		"""

		self.connection = connection

	def get_push(self):

		"""Returns a Booleam reflecting the state: True for push mode,
		False for pull mode (Boolean)
		"""

		# send the request
		response = self.connection.request('tracker', 'get', ['push'])
		# return value or error
		if response['statuscode'] == 200:
			return response['values']['push']
		else:
			raise Exception("Error in tracker.get_push: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def get_heartbeatinterval(self):

		"""Returns the expected heartbeat interval in milliseconds
		(integer)
		"""

		# send the request
		response = self.connection.request('tracker', 'get', ['heartbeatinterval'])
		# check if the tracker is in push mode
		if response['statuscode'] == 200:
			return response['values']['heartbeatinterval']
		else:
			raise Exception("Error in tracker.get_heartbeatinterval: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def get_version(self):

		"""Returns the version number (integer)
		"""

		# send the request
		response = self.connection.request('tracker', 'get', ['version'])
		# return value or error
		if response['statuscode'] == 200:
			return response['values']['version']
		else:
			raise Exception("Error in tracker.get_version: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def get_trackerstate(self):

		"""Returns the state of the physcial tracker (integer):
			0:	TRACKER_CONNECTED
				tracker is detected and working
			1:	TRACKER_NOT_CONNECTED
				tracker device is not connected
			2:	TRACKER_CONNECTED_BADFW
				tracker device is connected, but not working due to
				bad firmware
			3:	TRACKER_CONNECTED_NOUSB3
				tracker device is connected, but not working due to
				unsupported USB host
			4:	TRACKER_CONNECTED_NOSTREAM
				tracker device is connected, but no stream could be
				received
		"""

		# send the request
		response = self.connection.request('tracker', 'get', ['trackerstate'])
		# return value of error
		if response['statuscode'] == 200:
			return response['values']['trackerstate']
		else:
			raise Exception("Error in tracker.get_trackerstate: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def get_framerate(self):

		"""Returns the frame rate that the tracker is running at (integer)
		"""

		# send the request
		response = self.connection.request('tracker', 'get', ['framerate'])
		# return value or error
		if response['statuscode'] == 200:
			return response['values']['framerate']
		else:
			raise Exception("Error in tracker.get_framerate: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def get_iscalibrated(self):

		"""Indicates whether there is a calibration (Boolean)
		"""

		# send the request
		response = self.connection.request('tracker', 'get', ['iscalibrated'])
		# return value or error
		if response['statuscode'] == 200:
			return response['values']['iscalibrated']
		else:
			raise Exception("Error in tracker.get_iscalibrated: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def get_iscalibrating(self):

		"""Indicates whether the tracker is in calibration mode (Boolean)
		"""

		# send the request
		response = self.connection.request('tracker', 'get', ['iscalibrating'])
		# return value or error
		if response['statuscode'] == 200:
			return response['values']['iscalibrating']
		else:
			raise Exception("Error in tracker.get_iscalibrating: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def get_calibresult(self):

		"""Gets the latest valid calibration result

		returns

		WITHOUT CALIBRATION:
		None

		WITH CALIBRATION:
		calibresults	--	a dict containing the calibration results:
						{	'result':	Boolean indicating whether the
									calibration was succesful
							'deg':	float indicating the average error
									in degrees of visual angle
							'Ldeg':	float indicating the left eye
									error in degrees of visual angle
							'Rdeg':	float indicating the right eye
									error in degrees of visual angle
							'calibpoints': list, containing a dict for
										each calibration point:
										{'state':	integer indicating
												the state of the
												calibration point
												(0 means no useful
												data has been
												obtained and the
												point should be
												resampled; 1 means
												the data is of
												questionable
												quality, consider
												resampling; 2 means
												the data is ok)
										'cpx':	x coordinate of the
												calibration point
										'cpy':	y coordinate of the
												calibration point
										'mecpx':	mean estimated x
												coordinate of the
												calibration point
										'mecpy':	mean estimated y
												coordinate of the
												calibration point
										'acd':	float indicating
												the accuracy in
												degrees of visual
												angle
										'Lacd':	float indicating
												the accuracy in
												degrees of visual
												angle (left eye)
										'Racd':	float indicating
												the accuracy in
												degrees of visual
												angle (right eye)
										'mepix':	mean error in
												pixels
										'Lmepix':	mean error in
												pixels (left eye)
										'Rmepix':	mean error in
												pixels (right eye)
										'asdp':	standard deviation
												in pixels
										'Lasdp':	standard deviation
												in pixels (left eye)
										'Rasdp':	standard deviation
												in pixels (right eye)
										}
						}

		"""

		# send the request
		response = self.connection.request('tracker', 'get', ['calibresult'])

		# return value or error
		if response['statuscode'] != 200:
			raise Exception("Error in tracker.get_calibresult: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

		# return True if this was not the final calibration point
		if not 'calibresult' in response['values']:
			return None

		# if this was the final calibration point, return the results
		else:
			# return calibration dict
			returndict =  {	'result':response['values']['calibresult']['result'],
							'deg':response['values']['calibresult']['deg'],
							'Rdeg':response['values']['calibresult']['degl'],
							'Ldeg':response['values']['calibresult']['degr'],
							'calibpoints':[]
							}
			for pointdict in response['values']['calibresult']['calibpoints']:
				returndict['calibpoints'].append({	'state':pointdict['state'],
											'cpx':pointdict['cp']['x'],
											'cpy':pointdict['cp']['y'],
											'mecpx':pointdict['mecp']['x'],
											'mecpy':pointdict['mecp']['y'],
											'acd':pointdict['acd']['ad'],
											'Lacd':pointdict['acd']['adl'],
											'Racd':pointdict['acd']['adr'],
											'mepix':pointdict['mepix']['mep'],
											'Lmepix':pointdict['mepix']['mepl'],
											'Rmepix':pointdict['mepix']['mepr'],
											'asdp':pointdict['asdp']['asd'],
											'Lasdp':pointdict['asdp']['asdl'],
											'Rasdp':pointdict['asdp']['asdr']
											})
			return returndict

	def get_frame(self):

		"""Returns the latest frame data (dict)
			{	'timestamp': string time representation,
				'time': integer timestamp in milliseconds,
				'fix': Boolean indicating whether there is a fixation,
				'state': integer 32bit masked tracker state,
				'rawx': integer raw x gaze coordinate in pixels,
				'rawy': integer raw y gaze coordinate in pixels,
				'avgx': integer smoothed x gaze coordinate in pixels,
				'avgx': integer smoothed y gaze coordinate in pixels,
				'psize': float average pupil size,
				'Lrawx': integer raw x left eye gaze coordinate in pixels,
				'Lrawy': integer raw y left eye gaze coordinate in pixels,
				'Lavgx': integer smoothed x left eye gaze coordinate in pixels,
				'Lavgx': integer smoothed y left eye gaze coordinate in pixels,
				'Lpsize': float left eye pupil size,
				'Lpupilx': integer raw left eye pupil centre x coordinate,
				'Lpupily': integer raw left eye pupil centre y coordinate,
				'Rrawx': integer raw x right eye gaze coordinate in pixels,
				'Rrawy': integer raw y right eye gaze coordinate in pixels,
				'Ravgx': integer smoothed x right eye gaze coordinate in pixels,
				'Ravgx': integer smoothed y right eye gaze coordinate in pixels,
				'Rpsize': float right eye pupil size,
				'Rpupilx': integer raw right eye pupil centre x coordinate,
				'Rpupily': integer raw right eye pupil centre y coordinate
				}
		"""

		# send the request
		response = self.connection.request('tracker', 'get', ['frame'])
		# raise error if needed
		if response['statuscode'] != 200:
			raise Exception("Error in tracker.get_frame: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))
		# calculate pupil size
		# if both eyes are available, take the average
		if response['values']['frame']['lefteye']['psize'] > 0 and \
			response['values']['frame']['righteye']['psize'] > 0:
			psize = (response['values']['frame']['lefteye']['psize'] + \
				response['values']['frame']['righteye']['psize']) / 2.0
		# if only the right eye is available, then use the right eye
		elif response['values']['frame']['lefteye']['psize'] == 0 and \
			response['values']['frame']['righteye']['psize'] > 0:
			psize = response['values']['frame']['righteye']['psize']
		# if only the left eye is available, then use the left eye
		elif response['values']['frame']['lefteye']['psize'] > 0 and \
			response['values']['frame']['righteye']['psize'] == 0:
			psize = response['values']['frame']['lefteye']['psize']
		# if neither eye is available, then use the EyeTribe's standard
		# missing value (0.0)
		else:
			psize = 0.0
		# return the data in a dict
		return {	'timestamp':	response['values']['frame']['timestamp'],
				'time':		response['values']['frame']['time'],
				'fix':		response['values']['frame']['fix'],
				'state':		response['values']['frame']['state'],
				'rawx':		response['values']['frame']['raw']['x'],
				'rawy':		response['values']['frame']['raw']['y'],
				'avgx':		response['values']['frame']['avg']['x'],
				'avgy':		response['values']['frame']['avg']['y'],
				'psize':		psize,
				'Lrawx':		response['values']['frame']['lefteye']['raw']['x'],
				'Lrawy':		response['values']['frame']['lefteye']['raw']['y'],
				'Lavgx':		response['values']['frame']['lefteye']['avg']['x'],
				'Lavgy':		response['values']['frame']['lefteye']['avg']['y'],
				'Lpsize':		response['values']['frame']['lefteye']['psize'],
				'Lpupilx':		response['values']['frame']['lefteye']['pcenter']['x'],
				'Lpupily':		response['values']['frame']['lefteye']['pcenter']['y'],
				'Rrawx':		response['values']['frame']['righteye']['raw']['x'],
				'Rrawy':		response['values']['frame']['righteye']['raw']['y'],
				'Ravgx':		response['values']['frame']['righteye']['avg']['x'],
				'Ravgy':		response['values']['frame']['righteye']['avg']['y'],
				'Rpsize':		response['values']['frame']['righteye']['psize'],
				'Rpupilx':		response['values']['frame']['righteye']['pcenter']['x'],
				'Rpupily':		response['values']['frame']['righteye']['pcenter']['y']
				}

	def get_screenindex(self):

		"""Returns the screen index number in a multi screen setup (integer)
		"""

		# send the request
		response = self.connection.request('tracker', 'get', ['screenindex'])
		# return value or error
		if response['statuscode'] == 200:
			return response['values']['screenindex']
		else:
			raise Exception("Error in tracker.get_screenindex: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def get_screenresw(self):

		"""Returns the screen resolution width in pixels (integer)
		"""

		# send the request
		response = self.connection.request('tracker', 'get', ['screenresw'])
		# return value or error
		if response['statuscode'] == 200:
			return response['values']['screenresw']
		else:
			raise Exception("Error in tracker.get_screenresw: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def get_screenresh(self):

		"""Returns the screen resolution height in pixels (integer)
		"""

		# send the request
		response = self.connection.request('tracker', 'get', ['screenresh'])
		# return value or error
		if response['statuscode'] == 200:
			return response['values']['screenresh']
		else:
			raise Exception("Error in tracker.get_screenresh: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def get_screenpsyw(self):

		"""Returns the physical screen width in meters (float)
		"""

		# send the request
		response = self.connection.request('tracker', 'get', ['screenpsyw'])
		# return value or error
		if response['statuscode'] == 200:
			return response['values']['screenpsyw']
		else:
			raise Exception("Error in tracker.get_screenpsyw: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def get_screenpsyh(self):

		"""Returns the physical screen height in meters (float)
		"""

		# send the request
		response = self.connection.request('tracker', 'get', ['screenpsyh'])
		# return value or error
		if response['statuscode'] == 200:
			return response['values']['screenpsyh']
		else:
			raise Exception("Error in tracker.get_screenpsyh: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def set_push(self, push=None):

		"""Toggles the push state, or sets the state to the passed value

		keyword arguments

		push		--	Boolean indicating the state: True for push,
											False for pull
											None to toggle current
		returns

		state	--	Boolean indicating the push state
		"""

		# check passed value
		if push == None:
			# toggle state
			self.push = self.push != True
		elif type(push) == bool:
			# set state to passed value
			self.push = push
		else:
			# error on anything other than None, True or False
			raise Exception("tracker.set_push: push keyword argument should be a Boolean or None, not '%s'" % push)

		# send the request
		response = self.connection.request('tracker', 'set', {'push':str(self.push).lower()})
		# return value or error
		if response['statuscode'] == 200:
			return self.push
		else:
			raise Exception("Error in tracker.set_push: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))


	def set_version(self, version):

		"""Set the protocol version

		arguments

		version	--	integer version number
		"""

		# send the request
		response = self.connection.request('tracker', 'set', {'version':version})
		# return value or error
		if response['statuscode'] == 200:
			return version
		else:
			raise Exception("Error in tracker.set_version: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def set_screenindex(self, index):

		"""Set the screen index

		arguments

		index	--	integer value indicating the index number of the
					screen that is to be used with the tracker
		"""

		# send the request
		response = self.connection.request('tracker', 'set', {'screenindex':index})
		# return value or error
		if response['statuscode'] == 200:
			return index
		else:
			raise Exception("Error in tracker.set_screenindex: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def set_screenresw(self, width):

		"""Set the screen resolution width

		arguments

		width	--	integer value indicating the screen resolution width
					in pixels
		"""

		# send the request
		response = self.connection.request('tracker', 'set', {'screenresw':width})
		# return value or error
		if response['statuscode'] == 200:
			return width
		else:
			raise Exception("Error in tracker.set_screenresw: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def set_screenresh(self, height):

		"""Set the screen resolution height

		arguments

		height	--	integer value indicating the screen resolution height
					in pixels
		"""

		# send the request
		response = self.connection.request('tracker', 'set', {'screenresh':height})
		# return value or error
		if response['statuscode'] == 200:
			return height
		else:
			raise Exception("Error in tracker.set_screenresh: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def set_screenpsyw(self, width):

		"""Set the physical width of the screen

		arguments

		width	--	float value indicating the physical screen width in
					metres
		"""

		# send the request
		response = self.connection.request('tracker', 'set', {'screenpsyw':width})
		# return value or error
		if response['statuscode'] == 200:
			return width
		else:
			raise Exception("Error in tracker.set_screenpsyw: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def set_screenpsyh(self, height):

		"""Set the physical height of the screen

		arguments

		width	--	float value indicating the physical screen height in
					metres
		"""

		# send the request
		response = self.connection.request('tracker', 'set', {'screenpsyh':height})
		# return value or error
		if response['statuscode'] == 200:
			return height
		else:
			raise Exception("Error in tracker.set_screenpsyh: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))


class calibration:

	"""class for calibration related requests"""

	def __init__(self, connection):

		"""Initializes a calibration instance

		arguments

		connection	--	a pytribe.connection instance for the currently
						attached EyeTribe tracker
		"""

		self.connection = connection

	def set_connection(self, connection):

		"""Set a new connection

		arguments

		connection	--	a pytribe.connection instance for the currently
						attached EyeTribe tracker
		"""

		self.connection = connection

	def start(self, pointcount=9, max_attempts=5):

		"""Starts the calibration, using the passed number of calibration
		points

		keyword arguments

		pointcount	--	integer value indicating the amount of
						calibration points that should be used, which
						should be at least 7 (default = 9)
		max_attempts --	the number of times that calibration should be restarted
						if starting the calibration fails (default=5)
		"""
		
		for attempt in range(max_attempts):
		    # send the request
		    response = self.connection.request('calibration', 'start',
				{'pointcount':pointcount})
		    # return value or error
		    if response['statuscode'] == 200:
		        return True
		    self.abort()
		raise Exception("Error in calibration.start: %s (code %d)" \
			% (response['values']['statusmessage'],response['statuscode']))		

	def pointstart(self, x, y):

		"""Mark the beginning of a new calibration point for the tracker to
		process

		arguments

		x			--	integer indicating the x coordinate of the
						calibration point
		y			--	integer indicating the y coordinate of the
						calibration point

		returns

		success		--	Boolean: True on success, False on a failure
		"""

		# send the request
		response = self.connection.request('calibration', 'pointstart', {'x':x,'y':y})
		# return value or error
		if response['statuscode'] == 200:
			return True
		else:
			raise Exception("Error in calibration.pointstart: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def pointend(self):

		"""Mark the end of processing a calibration point

		returns

		NORMALLY:
		success		--	Boolean: True on success, False on failure

		AFTER FINAL POINT:
		calibresults	--	a dict containing the calibration results:
						{	'result':	Boolean indicating whether the
									calibration was succesful
							'deg':	float indicating the average error
									in degrees of visual angle
							'Ldeg':	float indicating the left eye
									error in degrees of visual angle
							'Rdeg':	float indicating the right eye
									error in degrees of visual angle
							'calibpoints': list, containing a dict for
										each calibration point:
										{'state':	integer indicating
												the state of the
												calibration point
												(0 means no useful
												data has been
												obtained and the
												point should be
												resampled; 1 means
												the data is of
												questionable
												quality, consider
												resampling; 2 means
												the data is ok)
										'cpx':	x coordinate of the
												calibration point
										'cpy':	y coordinate of the
												calibration point
										'mecpx':	mean estimated x
												coordinate of the
												calibration point
										'mecpy':	mean estimated y
												coordinate of the
												calibration point
										'acd':	float indicating
												the accuracy in
												degrees of visual
												angle
										'Lacd':	float indicating
												the accuracy in
												degrees of visual
												angle (left eye)
										'Racd':	float indicating
												the accuracy in
												degrees of visual
												angle (right eye)
										'mepix':	mean error in
												pixels
										'Lmepix':	mean error in
												pixels (left eye)
										'Rmepix':	mean error in
												pixels (right eye)
										'asdp':	standard deviation
												in pixels
										'Lasdp':	standard deviation
												in pixels (left eye)
										'Rasdp':	standard deviation
												in pixels (right eye)
										}
						}
		"""

		# send the request
		response = self.connection.request('calibration', 'pointend', None)
		# return value or error
		if response['statuscode'] != 200:
			raise Exception("Error in calibration.pointend: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

		# return True if this was not the final calibration point
		if ('values' not in response.keys()) or ('calibresult' not in response['values'].keys()):
			return True

		# if this was the final calibration point, return the results
		else:
			# return calibration dict
			returndict =  {	'result':response['values']['calibresult']['result'],
							'deg':response['values']['calibresult']['deg'],
							'Rdeg':response['values']['calibresult']['degl'],
							'Ldeg':response['values']['calibresult']['degr'],
							'calibpoints':[]
							}
			for pointdict in response['values']['calibresult']['calibpoints']:
				returndict['calibpoints'].append({	'state':pointdict['state'],
											'cpx':pointdict['cp']['x'],
											'cpy':pointdict['cp']['y'],
											'mecpx':pointdict['mecp']['x'],
											'mecpy':pointdict['mecp']['y'],
											'acd':pointdict['acd']['ad'],
											'Lacd':pointdict['acd']['adl'],
											'Racd':pointdict['acd']['adr'],
											'mepix':pointdict['mepix']['mep'],
											'Lmepix':pointdict['mepix']['mepl'],
											'Rmepix':pointdict['mepix']['mepr'],
											'asdp':pointdict['asdp']['asd'],
											'Lasdp':pointdict['asdp']['asdl'],
											'Rasdp':pointdict['asdp']['asdr']
											})
			return returndict

	def abort(self):

		"""Cancels the ongoing sequence and reinstates the previous
		calibration (only if there is one!)

		returns

		success		--	Boolean: True on success, False on failure
		"""

		# send the request
		response = self.connection.request('calibration', 'abort', None)
		# return value or error
		if response['statuscode'] == 200:
			return True
		else:
			raise Exception("Error in calibration.abort: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))

	def clear(self):

		"""Removes the current calibration from the tracker

		returns

		success		--	Boolean: True on success, False on failure
		"""

		# send the request
		response = self.connection.request('calibration', 'clear', None)
		# return value or error
		if response['statuscode'] == 200:
			return True
		else:
			raise Exception("Error in calibration.clear: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))


class heartbeat:

	"""class for signalling heartbeats to the server"""

	def __init__(self, connection):

		"""Initializes a heartbeat instance (not implemented in the SDK yet)

		arguments

		connection	--	a pytribe.connection instance for the currently
						attached EyeTribe tracker
		"""

		self.connection = connection

	def set_connection(self, connection):

		"""Set a new connection

		arguments

		connection	--	a pytribe.connection instance for the currently
						attached EyeTribe tracker
		"""

		self.connection = connection

	def beat(self):

		"""Sends a heartbeat to the device
		"""

		# send the request
		response = self.connection.request('heartbeat', None, None)
		# return value or error
		if response['statuscode'] == 200:
			return True
		else:
			raise Exception("Error in heartbeat.beat: %s (code %d)" % (response['values']['statusmessage'],response['statuscode']))


# # # # #
# DEBUG #
if __name__ == "__main__":
	test = EyeTribe()
	test.start_recording()
	time.sleep(10)
	test.stop_recording()
	test.close()
# # # # #
